// 
//   Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
//  
//   This program is free software: you can redistribute it and/or modify
//   it under the terms of the GNU Affero General Public License as
//   published by the Free Software Foundation, either version 3 of the
//   License, or (at your option) any later version.
//  
//   This program is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU Affero General Public License for more details.
//  
//   You should have received a copy of the GNU Affero General Public License
//   along with this program.  If not, see <http://www.gnu.org/licenses/>.
//  
//  


using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;

using Anculus.Core;

using Galaxium.Protocol.Xmpp.Library.Xml;
using Galaxium.Protocol.Xmpp.Library.Core;

namespace Galaxium.Protocol.Xmpp.Library.Streams
{
	public class InBandBytestreamManager: IBytestreamManager, IMessageListener
	{
		public string Identifier { get { return Namespaces.InBandBytestream; }}
		
		public Client Client { get; private set; }
		
		private Dictionary<string, InBandBytestream> _active_streams;
		private Dictionary<string, StreamReceivedHandler> _handlers;
		
		public InBandBytestreamManager ()
		{
			_active_streams = new Dictionary<string, InBandBytestream> ();
			_handlers = new Dictionary<string, StreamReceivedHandler> ();
		}
		
		public void Attach (Client client)
		{
			Detach ();
			Client = client;
			Client.DefineSetQueryHandler (Namespaces.InBandBytestream, HandleQuery);
			Client.RegisterFeature (Namespaces.InBandBytestream);
		}
		
		public void Detach ()
		{
			if (Client == null) return;
			
			foreach (var stream in _active_streams.Values)
				stream.Close ();
			
			_active_streams.Clear ();
			_handlers.Clear ();
			
			Client.UnregisterFeature (Namespaces.InBandBytestream);
			Client.DefineSetQueryHandler (Namespaces.InBandBytestream, null);
			Client = null;
		}
		
		private void HandleQuery (Iq query)
		{
			switch (query.Query.Name) {
				case "open": HandleStreamRequest (query); break;
				case "data": HandleData (query, query.Query); break;
				case "close": HandleClose (query); break;
			}
		}
		
		bool IMessageListener.HandleMessage (XmlMessage message)
		{
			var msg_id = message.ID;
			if (message.IsError && msg_id != null && msg_id.StartsWith ("ibb::"))
				HandleMessageError (message);
			
			var child = message.FirstChild ("data", Namespaces.InBandBytestream);
			if (child == null) return false;
			HandleData (message, child);
			return true;
		}
		
		// message ID format:  "ibb::" + sid + "::" + seq as four character lowercase hex number
		
		private void HandleMessageError (XmlMessage message)
		{
			var from = message ["from"];
			var msg_id = message ["id"];
			
			if (msg_id.Length < 12) return;
			
			var sid = msg_id.Substring (5, msg_id.Length - 11);
			
			var id = from + '#' + sid;
			
			InBandBytestream stream;
			if (_active_streams.TryGetValue (id, out stream)) {
				var thread = new Thread (() => {
					try { stream.Close (); }
					catch {}
				});
				thread.IsBackground = true;
				thread.Start ();
			}
		}
		
		private void HandleData (Stanza stanza, Element data_elm)
		{
			Log.Debug ("Handling data in " +
			           (stanza is Iq ? "IQ" : "message") + " from " + stanza ["from"]);
			
			InBandBytestream stream;
			var id = stanza ["from"] + '#' + data_elm ["sid"];
			if (_active_streams.TryGetValue (id, out stream)) {
				byte[] data;
				try { data = Convert.FromBase64String (data_elm.Text); }
				catch {
					Client.Send (stanza.CreateErrorResponse ("bad-request"));
					return;
				}
				try { stream.HandleData (Int32.Parse (data_elm ["seq"]), data); }
				catch (InvalidDataException) {
					Client.Send (stanza.CreateErrorResponse ("unexpected-request"));
					stream.Close ();
					return;
				}
				if (stanza is Iq)
					Client.Send ((stanza as Iq).Response ());
			}
			else {
				Client.Send (stanza.CreateErrorResponse ("item-not-found"));
			}
		}
		
		private void HandleClose (Iq query)
		{
			Log.Info ("Handling close from " + query.From);
			
			InBandBytestream stream;
			var id = query ["from"] + '#' + query.Query ["sid"];
			if (_active_streams.TryGetValue (id, out stream)) {
				stream.HandleClosed ();
				_active_streams.Remove (id);
				Client.Send (query.Response ());
			}
			else {
				Client.Send (query.CreateErrorResponse ("item-not-found"));
			}
		}
		
		private void HandleStreamRequest (Iq query)
		{
			Log.Info ("Handling open from " + query.From);
			
			var uid = query.From;
			
			var open = query.FirstChild ("open", Namespaces.InBandBytestream);
			var sid = open ["sid"];
			var block_size = Int32.Parse (open ["block-size"]);
			var in_messages = open ["stanza"] == "message";
			
			StreamReceivedHandler handler;
			if (!_handlers.TryGetValue (uid + '#' + sid, out handler)) {
				Client.Send (query.CreateErrorResponse ("unexpected-request"));
				return;
			}
			
			if (block_size > 16 * 1024) {  // 16kB chunk limit
				var description = "libStarLight has 16kB chunk limit";
				Client.Send (query.CreateErrorResponse ("resource-constraint", description));
				return;
			}
			
			Log.Info ("Request seems all right, initiating IBB stream.");
			
			var stream = new InBandBytestream (Client, this, uid, sid,
			                                   (ushort) block_size, in_messages);
			_active_streams [uid + '#' + sid] = stream;
			
			Log.Info ("Stream created.");
			
			handler.Invoke (StreamRequestResult.Success, stream);
			
			Client.Send (query.Response ());
		}
		
		internal void CloseStream (JabberID uid, string sid)
		{
			if (!_active_streams.ContainsKey (uid + '#' + sid))
				throw new InvalidOperationException ("No stream with that UID and SID");
			
			var iq = new Iq (IqType.Set, "close", Namespaces.InBandBytestream);
			iq.To = uid;
			iq.Query ["sid"] = sid;
			Client.Send (iq);
			
			_active_streams.Remove (uid + '#' + sid);
		}
		
		public void RequestStream (JabberID uid, string sid, StreamReceivedHandler handler)
		{
			RequestStream (uid, sid, 4096, false, handler);
		}
		
		public void RequestStream (JabberID uid, string sid, ushort block_size,
		                           bool use_messages, StreamReceivedHandler handler)
		{
			if (Client == null)
				throw new InvalidOperationException ("Not attached to any client.");
			
			var id = uid + '#' + sid;
			
			var query = new Iq (IqType.Set, "open", Namespaces.InBandBytestream);
			query.To = uid;
			query.Query ["sid"] = sid;
			if (use_messages)
				query.Query ["stanza"] = "message";
			
			var thread = new Thread (() => {
				InBandBytestream stream;
				while (true) {
					query.Query ["block-size"] = block_size.ToString ();
					stream = new InBandBytestream (Client, this, uid, sid, block_size, false);
					_active_streams.Add (id, stream);
					try {
						Client.SendQuery (query, 30000);
						handler (StreamRequestResult.Success, stream);
						return;
					}
					catch (TimeoutException) {
						_active_streams.Remove (id);
						handler (StreamRequestResult.TimedOut, null);
						return;
					}
					catch (QueryException e) {
						_active_streams.Remove (id);
						switch (e.Condition) {
							case "resource-constraint":
								if (block_size > 1) {
									block_size >>= 1;
									continue;
								}
								handler (StreamRequestResult.UnknownFailure, null);
								return;
							case "service-unavailable":
							case "feature-not-implemented":
								handler (StreamRequestResult.Unsupported, null);
								return;
							case "not-acceptable":
								handler (StreamRequestResult.Rejected, null);
								return;
							default:
								handler (StreamRequestResult.UnknownFailure, null);
								return;
						}
					}
				}
			});
			thread.IsBackground = true;
			thread.Start ();
		}
		
		public void ExpectStream (JabberID uid, string sid, StreamReceivedHandler handler)
		{
			if (Client == null)
				throw new InvalidOperationException ("Not attached to any client.");
			// TODO: add timeout
			_handlers.Add (uid + '#' + sid, handler);
		}
	}
}
